# 一、IO模型
Netty采用NIO模型(多路复用)进行IO线程的管理,底层是基于JDK的NIO接口进行开发,封装了一套易于调用的借口,并避免了原生JDK NIO接口的一些问题。
# NIO, BIO, AIO
Linux的五种IO模型的区别(磁盘IO)

Java中三种IO模型的区别(BIO、NIO、AIO)
Java中的三种IO模型都是依赖操作系统IO进行的封装,
BIO:同步阻塞型IO(1请求1线程连接),连接建立后用户线程发起IO请求,系统切换到内核态读取磁盘数据到内核缓冲区,此过程中用户线程阻塞等待直到内核缓冲区数据读取完成,再将其复制到用户态缓冲区(再到socket缓冲区)
NIO:同步非阻塞型IO(多请求1线程连接),连接建立后,用户线程发起IO请求并注册到selector(多路复用器)中,注册完用户线程返回,由Selector( Selector 其实就是 select/poll/epoll 的外包类)中的线程去检查多路连接的IO是否准备就绪,完毕则调用用户线程将内核缓冲区的数据复制到用户缓冲区。与上面的Linux模型中的普通NIO(不断轮询查看)不同,Java中的NIO是【多路复用IO+信号量IO】,大大减少用户线程的消耗,但是数据从内核到用户的复制仍然由用户线程执行,所以本质上仍然是同步的。

AIO:非同步非阻塞型IO。与同步型IO不同,内核态到用户态的数据复制在AIO中直接由内核线程负责处理,所以对于用户线程来说,是异步执行的。
# 二、线程模型
# Reactor线程模型
Reactor核心组成部分包括Selector、Acceptor和Handler,其中
selector 监控连接事件(read/write事件、连接事件),收到事件后通过dispatch进行分发
连接事件:分发到acceptor通过accept接收连接,并创建Handler。
读写事件:分发到Handler进行处理,Handler完成read->(decode->compute->encode)->send的业务流程
而根据Reactor的数量和工作线程池的数量,又将Reactor分为三种模型。
单Reactor单线程模型 (单Reactor单线程):单线程处理连接事件、读写事件、监听事件等,如果处理Handler出现死循环,将导致整个服务不可用。

单Reactor多线程模型 (单Reactor多线程【处理Handler的线程池】):业务处理由线程池异步执行,但是在大量高并发的场景下,单Reactor仍然会有性能问题。

主从Reactor多线程模型 (多Reactor多线程) :主从职责分离,主负责连接建立及验证,从负责事件监听、分发,是目前比较流行的线程模型。

主Reactor:通过selector监听连接事件,并调用acceptor创建连接并发送给从Reactor 从Reactor:接收到主Reactor的连接后加入队列,通过自己的Selector监听IO管道,并将读写事件分发给Handler的TheadPool,由该线程池做Handler的处理
三种模型详细介绍
- 单线程模型:所有I/O操作都由一个线程完成,即多路复用、事件分发和处理都 是在一个Reactor线程上完成的。既要接收客户端的连接请求,向服务端发起连 接,又要发送/读取请求或应答/响应消息。一个NIO 线程同时处理成百上千的 链路,性能上无法支撑,速度慢,若线程进入死循环,整个程序不可用,对于高负载、大并发的应用场景不合适。
- 多线程模型:有一个NIO 线程(Acceptor) 只负责监听服务端,接收客户端的 TCP 连接请求;NIO 线程池负责网络IO 的操作,即消息的读取、解码、编码和 发送;1 个NIO 线程可以同时处理N 条链路,但是1 个链路只对应1 个NIO 线 程,这是为了防止发生并发操作问题。但在并发百万客户端连接或需要安全认证时,一个Acceptor 线程可能会存在性能不足问题。
- 主从多线程模型:Acceptor 线程用于绑定监听端口,接收客户端连接,将 SocketChannel 从主线程池的Reactor 线程的多路复用器上移除,重新注册到 Sub 线程池的线程上, 用于处理I/O 的读写等操作,从而保证mainReactor只负 责接入认证、握手等操作;
# EventLoop和Reactor的关系
EventLoop直译即事件轮询,是一种用于事件等待和处理的程序模型。在Netty中,EventLoop可以认为是Reactor 线程模型的事件处理引擎,每个EventLoop都维护一个Selector和TaskQueue,主要负责处理IO事件、普通任务和定时任务。Reactor推荐使用NioEventLoop,其底层基于JDK的Epoll实现,并且避免了JDK Epoll的漏洞。在我的理解上,Reactor、EventLoopGroup、EventLoop、Channel之间的关系如下:
Reactor可以简单的认为是EventLoopGroup,例如主从Reactor即有两个EventLoopGroup。
EventLoopGroup和EventLoop是一对多的关系,可以简单理解为线程池和线程的关系,只不过EventLoop有具体的实现,包括轮询(Selector)自己管理的任务队列(TaskQueue),对于从Reactor,其EventLoop的任务队列即Channel队列,轮询监听Channel的IO事件,并调用ChannelPipeline中的Handler进行处理,所以EventLoop和Channel是一对多的关系。

# 三、零拷贝
什么是零拷贝
零拷贝是个广义上的概念,可以认为只要能够减少不必要的 CPU 拷贝,都可以理解为是零拷贝。常见的零拷贝优化有以下:
- 操作系统级别的零拷贝
- 用户态的数据操作优化
DMA: 全称叫直接内存存取(Direct Memory Access),是一种允许外围设备直接访问系统主存的机制。DMA 接管了数据读写的工作,不需要 CPU 再参与 I/O 中断的处理,从而减轻了 CPU 的负担。
一次传统磁盘IO的过程
磁盘 - 内核 - 用户空间(一次读写时共涉及了4次上下文切换,2次 DMA 拷贝以及2次 CPU 拷贝。)

- 一次网络IO的过程:web服务区 - 内核 - 用户空间 - 网卡(如果web服务器涉及磁盘IO,则还需要添加磁盘IO过程)

零拷贝实现
- 用户态直接IO:用户空间直接访问硬件设备,通过DMA进行数据传输

- mmap:MMAP 是数据不会到达用户空间内存,只会存在于系统空间的内存上,用户空间与系统空间共用同一个缓冲区,两者通过映射关联。(发生了 4 次上下文切换 + 1 次 CPU 拷贝 + 2 次 DMA 拷贝)

sendfile: Linux 2.4 版本开始,减少了内核缓冲区到Socket缓冲区的一次CPU拷贝,采用传输数据描述信息,由网卡DMA直接进行内核缓冲区的数据拷贝。与 mmap 内存映射方式不同的是, sendfile() 调用中数据对用户空间是完全不可见的,可以认为是对nmap的优化。(sendfile再JDK的封装中即
FileChannel#transferTo方法)
Netty的零拷贝优化
Netty的零拷贝技术除了操作系统级别的功能封装,更多的是面向用户态的数据操作优化。
- 堆外内存:Netty发送和接收消息主要使用bytebuffer,bytebuffer使用堆外内存,避免 JVM 堆内存到堆外内存的数据拷贝。
- Composite Buffers:传统的bytebuffer,如果要合并两个bytebuffer,需要创建一个2*n大小的新数组,再将两个拷贝到新数组中,而Composite Buffers没有真正将多个bytebuffer组合起来,而是保存了他们的引用。
- FileChannel.transferTo:Netty底层调用了FileChannel的transferTo方法,该方法即上述的sendfile方式的零拷贝,是操作系统级别的零拷贝技术。
# 四、TCP 粘包/拆包
TCP是以流的方式来处理数据,一个完整的包可能会被TCP拆分成多个包进行发送,也可能把小的封装成一个大的数据包发送。
原因:
- 当我们的数据包大于TCP包数据内容空间,则发生拆包,小于则发生粘包。例如:服务端将三个数据包被组装成两个TCP包发送,则服务端发生了粘包,客户端接收到粘包则需要进行拆包处理。
解决:
- FixedLengthFrameDecoder:定长拆包器:应用层协议包简单,数据包长度固定,则可以使用。
- LineBasedFrameDecoder:行拆包器:数据包以换行符作为分隔。
- DelimiterBasedFrameDecoder:自定义分隔符分割器:是行拆分器的通用版本,可以自定义换行符。
- LengthFieldBasedFrameDecoder:基于长度域拆包器:自定义的协议中包含长度域字段,则可以用该拆包器。